#!/usr/bin/env python
"""Creates a PNG, PDF or PS file based on a SVG file that is created by SynTree.

This script calculates the position of every node based on the same algorithm used in SynTree.

Wish- & fixlist:
 - 	Wish: Add support for with multiple trees
 - 	Wish: Multidominance support
 - 	Wish: Add support to fix the Y height if a node has multiple lines of text (fix the whole outline of this)
 -  Fix: Height of the document is incorrect (lots of white space)
 -  Fix: Sometimes the nodes are being positioned incorrect

Changelog:
v 0.5:
- Added warnings on start up to notify about pending bugs

v 0.4:
- Draw lines between the different nodes
- Dendrogram support

v 0.3:
- Fixed bug where nodes with flowSpan tags were ignored
- Added a total margin to the final output

v 0.2:
- Initial release"""

import xml.sax
import cairo

__author__ = "Jan-Willem Maris"
__copyright__ = "Copyright 2013, Master thesis"
__license__ = "GPL"
__version__ = "0.5"
__maintainer__ = "Jan-Willem Maris"
__email__ = "jw.maris@gmail.com"
__status__ = "Development"

# Global constants
GTAG_CORRECTION = 2  # First two <g> tags won't count
MARGIN_BETWEEN_NODES_X = 50  # Margin between every node used in cairo (this value will be enumerated)
MARGIN_BETWEEN_NODES_Y = 80  # Margin between every node used in cairo (this value will be multiplied)

FONT_TYPE = 'Sans'
FONT_SIZE = 20

class ReadSVGFile(xml.sax.ContentHandler):
	"""Read the SVG file and stores the values within the <g> tag"""
	def __init__(self):
		"""Initialize variables used in functions in the ReadSVGFile() class"""
		self.gDepth = 0  # Keeps track of the current depth based on <g> tags
		self.maxgDepth = 0  # Keeps track of the current maximal self.gDepth
		
		self.tree = []  # List of tree nodes (objects), used throughout the whole code
		
		#self.tagName = ''  # The current tag, used throughout the reading process of the SVG file
		self.nodeID = ''  # The unique ID of the current node, used throughout the reading process of the SVG file
		
		self.flowSpanName = ''
		
	def startElement(self, _tagNameStart, someAttrs):
		"""Signals the start of an element in non-namespace mode.
		Documentation: http://docs.python.org/2/library/xml.sax.handler.html#xml.sax.handler.ContentHandler.startElement"""
		self.openTag = True  # Keeps track of the status of the opening tag used within the ReadSVGFile.characters() function
		self.tagName = _tagNameStart  # Current name of the tag
		
		if self.tagName == 'g':  # Check if the current tag is <g>
			print(self.gDepth*' ', self.tagName, '(ID:', someAttrs.getValue('id'), ', diepte:)', self.gDepth)  # Debug information, displays the tag name, depth and the unique ID belonging to this tag
			self.nodeID = someAttrs.getValue('id')  # Retrieves the unique ID of a node used in ReadSVGFile.characters()
			
			self.gDepth += 1  # Add one to the current amount of counted <g> tags
			
			# Check if the current amount of <g> tags is the deepest found
			if self.gDepth > self.maxgDepth:
				self.maxgDepth = self.gDepth
	
	def endElement(self, tagNameEnd):
		"""Signals the end of an element in non-namespace mode.
		Documentation: http://docs.python.org/2/library/xml.sax.handler.html#xml.sax.handler.ContentHandler.endElement"""
		self.openTag = False  # Keeps track of the status of the opening tag used within the ReadSVGFile.characters() function
		
		# At the end of the current <g> tag so the current depth will be set back by one
		if tagNameEnd == 'g':
			self.gDepth -= 1
	
	def characters(self, nodeValue):
		"""Receive notification of character data.
		Documentation: http://docs.python.org/2/library/xml.sax.handler.html#xml.sax.handler.ContentHandler.endElement"""
		if not self.flowSpanName:  # Set the variable to (temporary) store the flowSpan value
			self.flowSpanName = ''
		
		if self.tagName == 'flowPara' and self.openTag == True:  # Check if the current open tag (== True) is flowPara (where the node value is)
			print(self.gDepth*' ', "nodeValue:", nodeValue, "(corrected gDepth:", self.gDepth-GTAG_CORRECTION, ")")  # Debug information, shows the value of the node and the corrected gDepth
			
			parentID = svgHandler.getParent(self.gDepth-GTAG_CORRECTION-1)  # Get the parent of the current node, because we search for the parent, go 1 up in <g> tag depth
			self.tree.append(Nodes(self.nodeID, nodeValue, parentID, self.gDepth-GTAG_CORRECTION))  # Make a list of node objects
		# Check if the current tag is 'flowSpan', a tag used for extra markup within the 'flowSpan' tag
		elif self.tagName == 'flowSpan' and self.openTag == True:
			self.flowSpanName = self.flowSpanName+nodeValue  # Concatenation of all the flowSpan tags found within the current flowPara tag
		elif self.tagName == 'flowSpan' and self.openTag == False and self.flowSpanName:  # When no more flowSpan tags can be found
			parentID = svgHandler.getParent(self.gDepth-GTAG_CORRECTION-1)  # Get the parent of the current node, because we search for the parent, go 1 up in <g> tag depth
			self.tree.append(Nodes(self.nodeID, self.flowSpanName, parentID, self.gDepth-GTAG_CORRECTION))  # Make a list of node objects
			print(self.nodeID, self.flowSpanName, parentID, self.gDepth-GTAG_CORRECTION)
			self.flowSpanName = ''  # Reset the flowSpan variable
		
	def getParent(self, currentDepth):
		"""Returns the parent of the requested node"""
		parent = ''
		for node in self.tree:  # Iterate through the added nodes in the SVG file
			if node.nodeDepth == currentDepth:  # Check if the current depth in the loop is the same as the requested depth
				parent = node.nodeID
		
		return parent

class Nodes():
	"""This is the place were all the values for each node will be stored"""
	def __init__(self, _nodeID, _nodeValue, _nodeParent, _nodeDepth, _x = 0, _y = 0):
		"""Initialize variables used in functions in the Nodes() class"""
		self.nodeID = _nodeID  # The unique ID of the node
		self.nodeValue = _nodeValue  # The text value of the node
		self.nodeParent = _nodeParent  # The parent of the node
		self.nodeDepth = _nodeDepth  # The depth of the node
		self.x = _x  # The x position of the node
		self.y = _y  # The y position of the node
	
class DrawPNG():
	"""The different elements of the drawing process come together in this class"""
	def __init__(self):
		self.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 0, 0)  # Width of the surface to work with with best quality (FORMAT_ARGB32)
		self.ctx = cairo.Context(self.surface) # Initialize Cairo and the surface to work with
		# The fonttype and fontsize need to be initialized so the correct width will be computed
		self.ctx.select_font_face(FONT_TYPE)  # Set the font type
		self.ctx.set_font_size(FONT_SIZE)  # Set the font size
		
		self.totalWidth = 0
	
	def drawText(self, _allNodes, depthRequest):
		self.allNodes = _allNodes  # The list with all the nodes stored
		
		for v in self.allNodes:  # Loop through all the provided nodes
			if v.nodeDepth == depthRequest:  # Proceeds when the node is on the requested depth
				print('---------------------------------------------\nHuidige nodeValue:', v.nodeValue, 'en diepte:', v.nodeDepth)  # Debug information
				hasChild = generateFile.checkForChildren(v.nodeID)  # Check if the current node has a child (returns True/False)
				
				getXBasedOnNeighbours = 0
				getNeighboursList = []
				if hasChild == False:  # If there are no children, the x positions will be based on the neighbours, if there are any
					getNeighboursList = generateFile.getNodesSameDepth(nodeDepth, v.nodeID)  # Retrieves a list of neighbours at the same depth for the current node based on the ID
					getXBasedOnNeighbours = generateFile.getWidthNodesList(getNeighboursList)  # Retrieves the width of the list
				getX = getXBasedOnNeighbours
				
				if hasChild == False:  # If there are no children
					if not getNeighboursList:  # If there are no neighbours also on the left
						nodeWidth = generateFile.getWidthNode(v.nodeValue)  # Retrieves the width of the current node
						
						# Important process: the nodes on the right will be moved more (based on the own width and the margin) to the right, since this node popped in on a higher level
						for alreadyAddedNode in self.allNodes:
							alreadyAddedNode.x += nodeWidth + MARGIN_BETWEEN_NODES_X  # Add the extra margin to x position of the found node
							
					else:  # If there are no children, but there are neighbours on the left side we calculate the most right width of all the children (outerWidth)
						getX = generateFile.getMostOuterWidth(nodeDepth+1, getNeighboursList[-1])  # Retrieve the most right child of the neighbour before the current one and override the x value
						print('  <> Laagst liggende kind obv de resultaten:', getX)  # Debug information
						
						addToRight = generateFile.getWidthNode(v.nodeValue )  # Retrieves the width of the current node
						for node in reversed(self.allNodes):  # Reverse the list so the nodes on the will be first in the list (since the list is read from the SVG file top-down)
							if node.nodeID == v.nodeID:  # Until we're at the current node of all the nodes so the nodes on the left don't get moved
								break
							else:
								for alreadyAddedNode in self.allNodes:
									if alreadyAddedNode.nodeID == node.nodeID:
										print('  <> We voegen een beetje x (', alreadyAddedNode.x, ') toe aan:', alreadyAddedNode.nodeValue)  # Debug information
										alreadyAddedNode.x += addToRight + MARGIN_BETWEEN_NODES_X  # Add the extra margin to x position of the found node
										print('  <> De nieuwe x van:', alreadyAddedNode.nodeValue, 'is:', alreadyAddedNode.x)  # Debug information
				else:
					getXBasedOnChildren = generateFile.getWidthBasedOnChildren(v.nodeID)  # Gets the width of X based on the children of the current node based on the ID
					getX += getXBasedOnChildren
				
				if(v.nodeDepth == 1):  # When the root node is reached
					self.totalWidth = int(generateFile.getMostOuterWidth(2, v.nodeID) - MARGIN_BETWEEN_NODES_X)  # Get the most outer x of the most right child
				
				getY = depthRequest * MARGIN_BETWEEN_NODES_Y
				v.x = getX
				v.y = getY
				
		return self.allNodes
	
	def getWidthNode(self, nodeValue):
		"""Return the width as a number of a the value of a node (string)"""
		(x, y, width, height, dx, dy) = self.ctx.text_extents(nodeValue)  # Retrieves the width and other aspects of the node value
		return width
	
	def getHeightNode(self, nodeValue):
		"""Return the width as a number of a the value of a node (string)"""
		(x, y, width, height, dx, dy) = self.ctx.text_extents(nodeValue)  # Retrieves the width and other aspects of the node value
		return height
	
	def getWidthNodesList(self, neighboursList):
		"""Returns a int as the total width of the width of the provided list (getNeighboursList)"""
		lengthX = 0
		if neighboursList:  # If there are neighbours in the neighboursList list
			for item in self.allNodes:  # Loop through all the nodes
				if item.nodeID == neighboursList[-1]:  # If the node ID is the same as the most right child in the list of neighbours
					widthNode = generateFile.getWidthNode(item.nodeValue)
					lengthX = item.x + widthNode + (widthNode/2)  # Add the width of the node value to the x position to position the node
		
		return lengthX
	
	def getChildren(self, nodeID):
		"""Returns a list of nodes by looking at the already saved children"""
		listNodes = []
		# Loops through all the nodes and checks if the saved parent is the same the current value
		for node in self.allNodes:
			if node.nodeParent == nodeID:
				listNodes.append(node.nodeID)
		
		return listNodes
	
	def checkForChildren(self, nodeID):
		"""Returns True if there are children found for the given node ID and False if not"""
		listNodesChildren = generateFile.getChildren(nodeID)  # Get a list of node IDs of the children
		childStatus = False
		
		for child in listNodesChildren:
			for node in self.allNodes:
				if node.nodeID == child:  # Compare the ID of a node if it's the list with IDs
					childStatus = True
		
		return childStatus
	
	def getWidthBasedOnChildren(self, nodeID):
		"""Returns a number which is the total width of the children of a give node ID"""
		listNodesChildren = generateFile.getChildren(nodeID)  # Get a list of node IDs of the children
		lengthX = 0
		allXs = []
		
		for child in listNodesChildren:
			for alreadyNode in self.allNodes:
				if alreadyNode.nodeID == child:  # Compare the ID of a node if it's the list with IDs
					allXs.append(alreadyNode.x)  # Add the corresponding x value to the allXs list
		
		if len(allXs) > 1:  # When there are multiple x values in the list
			lengthX = (allXs[0] + allXs[len(allXs)-1]) / 2 + 5  # Get the first and last x value, divided by 2 and add 5 for a small correction
		elif len(allXs) > 0: # When there's only one x we can use that x
			lengthX = allXs[0]
		
		return lengthX
	
	def getNodesSameDepth(self, depthRequest, nodeID):
		"""Returns a list of IDs of the node(s) that are on the same depth on the left side of the current node to later calculate the width calculated from the left"""
		listNodes = []
		
		# Loop through all the nodes and adds these to the listNodes list until it reaches the same ID (or else the possible nodes on the right side also get's added)
		for node in self.allNodes:
			if node.nodeID == nodeID:
				break
			if node.nodeDepth == depthRequest:
				listNodes.append(node.nodeID)
		
		return listNodes
	
	def getMostOuterWidth(self, depthChildren, nodeID):
		"""Get the most right side in a list of nodes, so it will return the most outer width"""
		mostOuterX = 0  # The x position of the most right found node
		
		if generateFile.getChildren(nodeID):  # Check if the found node(s) have more children
			return generateFile.getMostOuterWidth(depthChildren-1, generateFile.getChildren(nodeID)[-1])  # Recursively keep on finding the most outer right child (that's why [-1] is used)
		else:
			# Find the details of the found most right node. Details will be retrieved from self.allNodes 
			for node in self.allNodes:
				if node.nodeID == nodeID:
					nodeWidth = generateFile.getWidthNode(node.nodeValue)  # Retrieve the width of the value of the node
					mostOuterX = node.x + nodeWidth + MARGIN_BETWEEN_NODES_X  # Add the x position, the value width and the margin
		
		return mostOuterX
	
	def finishPNG(self, filename, depthTree, filetype):
		"""Finishes the image file by putting all the node values on the right x and y place"""
		totalHeight = int(depthTree+1) * MARGIN_BETWEEN_NODES_Y  # The height of the document
		totalMargin = 5  # Adds a margin in the final document so the nodes don't stick to the sides of the image/pdf
		
		if filetype == 'png':
			treediagramSurface = cairo.ImageSurface(cairo.FORMAT_ARGB32, self.totalWidth+(2*totalMargin), totalHeight)  # Width and height of the surface to work with and the best quality (FORMAT_ARGB32)
			dendrogramSurface = cairo.ImageSurface(cairo.FORMAT_ARGB32, self.totalWidth+(2*totalMargin), totalHeight+100)  # Width and height of the surface to work with and the best quality (FORMAT_ARGB32)
		elif filetype in ('pdf', 'ps'):
			treediagramSurface = cairo.PDFSurface(filename + "." + filetype, self.totalWidth+(2*totalMargin), totalHeight)  # Width and height of the surface to work with and filename
			dendrogramSurface = cairo.PDFSurface(filename + "-dendrogram." + filetype, self.totalWidth+(2*totalMargin), totalHeight)  # Width and height of the surface to work with and filename
		
		# Draw the treediagram
		treediagram = cairo.Context(treediagramSurface)
		
		treediagram.select_font_face(FONT_TYPE)  # Set the font type
		treediagram.set_font_size(FONT_SIZE)  # Set the font size
		
		for item in self.allNodes:
			treediagram.move_to(item.x+totalMargin, item.y)  # Move to point (x, y)
			treediagram.show_text(item.nodeValue)  # Add the node value text at the right points
			
			treediagram.set_source_rgb(0, 0, 0)
			treediagram.set_line_width(0.8)
			
			if generateFile.checkForChildren(item.nodeID) == True:
				for child in generateFile.getChildren(item.nodeID):
					for itom in self.allNodes:
						if itom.nodeID == child:
							treediagram.move_to(item.x+(generateFile.getWidthNode(item.nodeValue)/2), item.y+5)
							treediagram.line_to(itom.x+(generateFile.getWidthNode(itom.nodeValue)/2), itom.y-generateFile.getHeightNode(itom.nodeValue)-5)
							treediagram.stroke()
		
		
		# Draw the dendrogram
		dendrogram = cairo.Context(dendrogramSurface)
		
		dendrogram.select_font_face(FONT_TYPE)  # Set the font type
		dendrogram.set_font_size(FONT_SIZE)  # Set the font size
		
		# Alle eindnodes verplaatsen naar laagst mogelijke punt
		dendroNodes = allNodes
		for count,dendrofix in enumerate(dendroNodes):
			if generateFile.checkForChildren(dendrofix.nodeID) == False:
				if count != 0:  # The first node is always the deepest
					for deepestNode in allNodes:
						if deepestNode.nodeDepth == svgHandler.maxgDepth-3:
							dendrofix.y = deepestNode.y
							break
				#dendrofix.y = (svgHandler.maxgDepth-3) * MARGIN_BETWEEN_NODES_Y  # -3 omdat de eerste 3 <g> tags niet meetellen
		
		for item in dendroNodes:
			dendrogram.move_to(item.x+totalMargin, item.y)  # Move to point (x, y)
			dendrogram.show_text(item.nodeValue)  # Add the node value text at the right points
			
			dendrogram.set_source_rgb(0, 0, 0)
			dendrogram.set_line_width(0.8)
			
			if generateFile.checkForChildren(item.nodeID) == True:
				for child in generateFile.getChildren(item.nodeID):
					for itom in self.allNodes:
						if itom.nodeID == child:
							# Opzij
							dendrogram.move_to(item.x+(generateFile.getWidthNode(item.nodeValue)/2), item.y+5)
							dendrogram.line_to(itom.x+(generateFile.getWidthNode(itom.nodeValue)/2), item.y+5)
							dendrogram.stroke()
							# Naar beneden
							dendrogram.move_to(itom.x+(generateFile.getWidthNode(itom.nodeValue)/2), item.y+5)
							dendrogram.line_to(itom.x+(generateFile.getWidthNode(itom.nodeValue)/2), itom.y-generateFile.getHeightNode(itom.nodeValue)-5)
							dendrogram.stroke()
		
		# Writes the file to the system disk
		if filetype == 'png':
			treediagramSurface.write_to_png(filename + "." + filetype)
			dendrogramSurface.write_to_png(filename + "-dendrogram." + filetype)
		elif filetype in ('pdf', 'ps'):
			treediagramSurface.show_page()
			dendrogramSurface.show_page()
		
		print("Successfully created", filename + "." + filetype)

def askFilename():
	"""Keeps on asking for a excisting filename for a SVG file in the same folder as this script is in"""
	print("Note: the requested SVG file must be in the same folder as this script")
	filename = input("Enter the filename of the SVG file to load (without the .svg suffix): ")
	try:
		with open(filename + '.svg'): pass
		return filename
	except IOError:
		print("\nError: could not find the file '" + filename + ".svg' in the current directory.")
		askFilename()

print("Note: the following bugs are still here:")
print(" -  Height of the document is incorrect (lots of white space)\n -  Fix: Sometimes the nodes are being positioned incorrect")
filename = askFilename()

print("What filetype should be the output?")
filetypes = ['PNG', 'PDF', 'PS']
for i in range(len(filetypes)):
	print("[", i+1, "] " + filetypes[i], sep='')
filetypeInput = eval(input("Enter a numer as input: "))
filetype = filetypes[filetypeInput-1].lower()

# Start the reading process of ReadSVGFile() by reading the filename
parser = xml.sax.make_parser()
svgHandler = ReadSVGFile()
parser.setContentHandler(svgHandler)
parser.parse(filename + '.svg')

generateFile = DrawPNG()

depthTree = svgHandler.maxgDepth-GTAG_CORRECTION
for nodeDepth in range(depthTree, 0, -1):  # Loop through the nodes bottom-up
	allNodes = generateFile.drawText(svgHandler.tree, nodeDepth)

generateFile.finishPNG(filename, depthTree, filetype)  # Generate the final file